package com.xyy.saas.payment.payment.core.service;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.lang.Pair;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.google.common.collect.Lists;
import com.xyy.saas.payment.common.ReconcileTradeConstants;
import com.xyy.saas.payment.common.exception.PaymentException;
import com.xyy.saas.payment.cores.enums.FbankTradeCodeEnum;
import com.xyy.saas.payment.cores.enums.ResultCodeEnum;
import com.xyy.saas.payment.dao.mapper.*;
import com.xyy.saas.payment.dao.model.PayReport;
import com.xyy.saas.payment.dao.model.ThirdUserInfo;
import com.xyy.saas.payment.dto.TradeAmountDto;
import com.xyy.saas.payment.payment.core.dto.ReconcileContext;
import com.xyy.saas.payment.payment.core.dto.ReconcileFundAccountDto;
import com.xyy.saas.payment.trace.AlarmService;
import com.xyy.saas.payment.trace.DingTalkMessage;
import com.xyy.saas.payment.util.AmountCastUtil;
import com.xyy.saas.payment.util.DateUtil;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author wangzeqi
 */
@Slf4j
@Service
public class FundAccountCheckService {

    @Autowired
    private PayAccountMapper payAccountMapper;
    @Autowired
    private ThirdUserInfoMapper thirdUserInfoMapper;
    @Autowired
    private PayReportMapper payReportMapper;
    @Autowired
    private SeparateOrderMapper separateOrderMapper;
    @Autowired
    private WithdrawOrderMapper withdrawOrderMapper;
    @Autowired
    private FbankPayRecordsMapper fbankPayRecordsMapper;
    @Resource
    private AlarmService alarmService;
    @Value("${reconcile.webChatWebHook:''}")
    private String webChatWebHook;
    @Value("${reconcile.webChatMobiles:''}")
    private String webChatMobiles;
    @Value("${reconcileFundAccountInterrupt.switch:false}")
    private Boolean reconcileFundAccountInterrupt;
    @Value("${reconcileFundAccountSleep:500}")
    private Integer reconcileFundAccountSleep;
    @Value("${reconcileFundAccountAlarm.switch:true}")
    private Boolean reconcileFundAccountAlarmSwitch;

    public void reconcileFundAccount(ReconcileContext context) {

        String logPrefix = "reconcileFundAccount - ";

        checkReconcileFundAccountParamDto(logPrefix, context);

        // pop账户资金核对
        try {
            long startTime = System.currentTimeMillis();
            reconcileEcPopFundAccount(logPrefix, context);
            log.info("{} Method reconcileEcPopFundAccount cost time: {}", logPrefix, System.currentTimeMillis() - startTime);
        } catch (Exception e) {
            log.error("{} Method reconcileEcPopFundAccount error, context:{}", logPrefix, JSON.toJSONString(context), e);
        }

        // 自营账户资金核对
        try {
            long startTime = System.currentTimeMillis();
            reconcileEcSelfSupportFundAccount(logPrefix, context);
            log.info("{} Method reconcileEcSelfSupportFundAccount cost time: {}", logPrefix, System.currentTimeMillis() - startTime);
        } catch (Exception e) {
            log.error("{} Method reconcileEcSelfSupportFundAccount error, context:{}", logPrefix, JSON.toJSONString(context), e);
        }
    }

    private void reconcileEcSelfSupportFundAccount(String logPrefix, ReconcileContext context) {
        Map<String, String> reconcileAccountMap = ReconcileTradeConstants.ecSelfSupportReconcileAccountMap;
        for (Map.Entry<String, String> entry : reconcileAccountMap.entrySet()) {

            if (Boolean.TRUE.equals(reconcileFundAccountInterrupt)) {
                log.error("reconcileEcSelfSupportFundAccount interrupt context:{}", JSON.toJSONString(context));
                break;
            }

            String accountId = entry.getKey();
            String thirdUserId = entry.getValue();

            try {
                processReconcileEcSelfSupportFundAccount(logPrefix, accountId, thirdUserId, context);
            } catch (Exception e) {
                log.error("{} processReconcileEcPopFundAccount error, accountId:{}, thirdUserId:{}, context:{}", logPrefix, accountId, thirdUserId, JSON.toJSONString(context), e);
                if (Boolean.TRUE.equals(reconcileFundAccountAlarmSwitch)) {
                    alarmService.alarm(webChatWebHook, webChatMobiles,
                            DingTalkMessage.builder()
                                    .type("自营账户资金对账执行失败")
                                    .method("processReconcileEcSelfSupportFundAccount")
                                    .msg("错误信息：" + e.getMessage())
                                    .param("accountId：" + accountId)
                                    .build());
                }
            }

            sleep(reconcileFundAccountSleep);
        }
    }

    private void processReconcileEcSelfSupportFundAccount(String logPrefix, String accountId, String thirdUserId, ReconcileContext context) {
        ReconcileFundAccountDto reconcileFundAccountDto = new ReconcileFundAccountDto();
        reconcileFundAccountDto.setAccountId(accountId);
        reconcileFundAccountDto.setThirdUserId(thirdUserId);

        // 支付报表账户日终余额
        fillReconcileFundAccountDtoByPayReport(logPrefix, reconcileFundAccountDto, context);
        // 按交易类型汇总金额
        fillSelfSupportReconcileFundAccountDtoByTradeSummary(reconcileFundAccountDto, context);
        // 理论变动值 vs 实际变动值
        reconcileDailyFundVariance(logPrefix, reconcileFundAccountDto, context);
    }

    private void fillSelfSupportReconcileFundAccountDtoByTradeSummary(ReconcileFundAccountDto reconcileFundAccountDto, ReconcileContext context) {
        // 获取交易类型对应的金额
        // key -> tradeCode   value -> amount
        Map<Integer, BigDecimal> tradeAmountMap = getSelfSupportTradeAmountMap(reconcileFundAccountDto, context);

        // 提现金额
        Long withdrawAmount = withdrawOrderMapper.queryWithdrawAmount(reconcileFundAccountDto.getAccountId(), context.getBeginTime(), context.getEndTime());

        // 分账金额
        reconcileFundAccountDto.setShareAmount(BigDecimal.ZERO);
        // 分账撤销金额
        reconcileFundAccountDto.setShareCancelAmount(BigDecimal.ZERO);
        // 会员间交易入账
        reconcileFundAccountDto.setMemberTradeInAmount(tradeAmountMap.getOrDefault(FbankTradeCodeEnum.FTC_1.getCode(), BigDecimal.ZERO));
        // 会员间交易退款
        reconcileFundAccountDto.setMemberTradeOutAmount(BigDecimal.ZERO);
        // 可提现解冻
        reconcileFundAccountDto.setUnfreezeAmount(BigDecimal.ZERO);
        // 可提现冻结
        reconcileFundAccountDto.setFreezeAmount(BigDecimal.ZERO);
        // 提现金额
        reconcileFundAccountDto.setWithdrawAmount(AmountCastUtil.castFen2Yuan2(withdrawAmount != null ? withdrawAmount : 0L));

        // 支付金额
        BigDecimal totalPayAmount = AmountCastUtil.sum(
                tradeAmountMap.getOrDefault(FbankTradeCodeEnum.FTC_4.getCode(), BigDecimal.ZERO),
                tradeAmountMap.getOrDefault(FbankTradeCodeEnum.FTC_9.getCode(), BigDecimal.ZERO)
        );
        reconcileFundAccountDto.setPayAmount(totalPayAmount);

        // 退款金额
        BigDecimal totalRefundAmount = AmountCastUtil.sum(
                tradeAmountMap.getOrDefault(FbankTradeCodeEnum.FTC_6.getCode(), BigDecimal.ZERO),
                tradeAmountMap.getOrDefault(FbankTradeCodeEnum.FTC_3.getCode(), BigDecimal.ZERO).abs()
        );
        reconcileFundAccountDto.setRefundAmount(totalRefundAmount);

        // 当日变动 = 入金 - 出金
        BigDecimal currentDayTotalInAmount = AmountCastUtil.sum(
                reconcileFundAccountDto.getMemberTradeInAmount(),
                reconcileFundAccountDto.getPayAmount()
        );
        BigDecimal currentDayTotalOutAmount = AmountCastUtil.sum(
                reconcileFundAccountDto.getRefundAmount(),
                reconcileFundAccountDto.getWithdrawAmount()
        );
        reconcileFundAccountDto.setTotalActualChangeAmount(currentDayTotalInAmount.subtract(currentDayTotalOutAmount));
    }

    private Map<Integer, BigDecimal> getSelfSupportTradeAmountMap(ReconcileFundAccountDto reconcileFundAccountDto, ReconcileContext context) {
        // 账户入金
        List<TradeAmountDto> tradeInAmountDtos = fbankPayRecordsMapper.queryInTotalAmountByTradeCode(
                reconcileFundAccountDto.getAccountId(), context.getBeginTime(), context.getEndTime(),
                ReconcileTradeConstants.selfSupportReconcileInFundAccountTradeCodes1);

        // 账户出金
        List<TradeAmountDto> tradeOutAmountDtos = fbankPayRecordsMapper.queryOutTotalAmountByTradeCode(
                reconcileFundAccountDto.getAccountId(), context.getBeginTime(), context.getEndTime(),
                Arrays.asList(FbankTradeCodeEnum.FTC_6.getCode()));

        // 汇总
        List<TradeAmountDto> allTradeAmountDtos = new ArrayList<>();
        allTradeAmountDtos.addAll(Optional.ofNullable(tradeInAmountDtos).orElse(Collections.emptyList()));
        allTradeAmountDtos.addAll(Optional.ofNullable(tradeOutAmountDtos).orElse(Collections.emptyList()));

        // key -> tradeCode   value -> amount
        Map<Integer, BigDecimal> tradeAmountMap = new HashMap<>();
        if (CollectionUtil.isNotEmpty(allTradeAmountDtos)) {
            tradeAmountMap = allTradeAmountDtos.stream().collect(Collectors.toMap(
                    TradeAmountDto::getTradeCode,
                    TradeAmountDto::getTradeAmount,
                    (k1, k2) -> k2));
        }
        return tradeAmountMap;
    }

    @SneakyThrows
    private void checkReconcileFundAccountParamDto(String logPrefix, ReconcileContext context) {
        if (context == null) {
            throw new PaymentException(ResultCodeEnum.PARAM_ERROR);
        }

        LocalDateTime beginTime = context.getBeginTime();
        LocalDateTime endTime = context.getEndTime();
        if (beginTime == null || endTime == null) {
            endTime = LocalDate.now().atStartOfDay();
            beginTime = endTime.minusDays(1);
        }

        if (beginTime.toLocalDate().equals(endTime.toLocalDate())) {
            throw new PaymentException(ResultCodeEnum.PARAM_ERROR);
        }

        if (Math.abs(ChronoUnit.DAYS.between(beginTime, endTime)) > 2) {
            log.error("{} Method checkReconcileTradeParamDto time diff days > 2", logPrefix);
            throw new PaymentException(ResultCodeEnum.PARAM_ERROR);
        }
    }

    private void reconcileEcPopFundAccount(String logPrefix, ReconcileContext context) {
        log.info("{} Method reconcileEcPopFundAccount start, context:{}", logPrefix, JSON.toJSONString(context));

        Map<String, String> reconcileAccountMap = getReconcilePopAccount(context);
        if (CollectionUtil.isEmpty(reconcileAccountMap)) {
            log.error("{} reconcileEcPopFundAccount reconcileAccountMap is empty, context:{}", logPrefix, JSON.toJSONString(context));
            return;
        }

        for (Map.Entry<String, String> entry : reconcileAccountMap.entrySet()) {

            if (Boolean.TRUE.equals(reconcileFundAccountInterrupt)) {
                log.error("reconcileEcPopFundAccount interrupt context:{}", JSON.toJSONString(context));
                break;
            }

            String accountId = entry.getKey();
            String thirdUserId = entry.getValue();
            if (StrUtil.isBlank(accountId) || StrUtil.isBlank(thirdUserId)) {
                continue;
            }

            try {
                processReconcileEcPopFundAccount(logPrefix, accountId, thirdUserId, context);
            } catch (Exception e) {
                log.error("{} processReconcileEcPopFundAccount error, accountId:{}, thirdUserId:{}, context:{}", logPrefix, accountId, thirdUserId, JSON.toJSONString(context), e);
                if (Boolean.TRUE.equals(reconcileFundAccountAlarmSwitch)) {
                    alarmService.alarm(webChatWebHook, webChatMobiles,
                            DingTalkMessage.builder()
                                    .type("POP账户资金对账执行失败")
                                    .method("processReconcileEcPopFundAccount")
                                    .msg("错误信息：" + e.getMessage())
                                    .param("accountId：" + accountId)
                                    .build());
                }
            }

            sleep(reconcileFundAccountSleep);
        }
    }

    private void processReconcileEcPopFundAccount(String logPrefix, String accountId, String thirdUserId, ReconcileContext context) {
        ReconcileFundAccountDto reconcileFundAccountDto = new ReconcileFundAccountDto();
        reconcileFundAccountDto.setAccountId(accountId);
        reconcileFundAccountDto.setThirdUserId(thirdUserId);

        // 支付报表账户日终余额
        fillReconcileFundAccountDtoByPayReport(logPrefix, reconcileFundAccountDto, context);

        // 按交易类型汇总金额
        fillPopReconcileFundAccountDtoByTradeSummary(reconcileFundAccountDto, context);

        // 理论变动值 vs 实际变动值
        reconcileDailyFundVariance(logPrefix, reconcileFundAccountDto, context);
    }

    private void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    private void reconcileDailyFundVariance(String logPrefix, ReconcileFundAccountDto reconcileFundAccountDto, ReconcileContext context) {
        // 理论变动值
        BigDecimal totalTheoryChangeAmount = reconcileFundAccountDto.getCurrentDayBalance().subtract(reconcileFundAccountDto.getLastDayBalance());
        // 实际变动值
        BigDecimal totalActualChangeAmount = reconcileFundAccountDto.getTotalActualChangeAmount();

        boolean reconcileSuc = totalActualChangeAmount.compareTo(totalTheoryChangeAmount) == 0;
        String reconcileResultDesc = reconcileSuc ? "平衡" : "不平衡";

        log.info("reconcileDailyFundVariance 账户:{}, {} 至 {} 理论变动金额:{}, 实际变动金额:{}, 核对结果:{}",
                reconcileFundAccountDto.getAccountId(), DateUtil.localDateTimeFormat(context.getBeginTime()), DateUtil.localDateTimeFormat(context.getEndTime()),
                AmountCastUtil.decimalFormat(totalTheoryChangeAmount), AmountCastUtil.decimalFormat(totalActualChangeAmount), reconcileResultDesc);

        if (!reconcileSuc) {
            if (Boolean.TRUE.equals(reconcileFundAccountAlarmSwitch)) {
                alarmService.alarm(webChatWebHook, webChatMobiles,
                        DingTalkMessage.builder()
                                .type("资金账户对账")
                                .method("reconcileDailyFundVariance")
                                .msg("账户余额对账不一致, " + DateUtil.localDateTimeFormat(context.getBeginTime()) + "至" + DateUtil.localDateTimeFormat(context.getEndTime())
                                        + "理论变动金额为" + AmountCastUtil.decimalFormat(totalTheoryChangeAmount) + ", " + "实际变动金额为" + AmountCastUtil.decimalFormat(totalActualChangeAmount))
                                .param("accountId: " + reconcileFundAccountDto.getAccountId())
                                .build());
            }
        }

    }

    private void fillPopReconcileFundAccountDtoByTradeSummary(ReconcileFundAccountDto reconcileFundAccountDto, ReconcileContext context) {
        // 会员间交易入账、分账、可提现解冻、会员间交易退款（平安贷）、分账撤销、可提现冻结
        List<TradeAmountDto> tradeAmountDtos = separateOrderMapper.queryTotalAmountByTradeCode(
                reconcileFundAccountDto.getAccountId(), context.getBeginTime(), context.getEndTime(),
                ReconcileTradeConstants.popReconcileFundAccountTradeCodes1);
        // tradeCode:amount
        Map<Integer, BigDecimal> tradeAmountMap = new HashMap<>();
        if (CollectionUtil.isNotEmpty(tradeAmountDtos)) {
            tradeAmountMap = tradeAmountDtos.stream().collect(Collectors.toMap(
                    TradeAmountDto::getTradeCode,
                    t -> t.getAmount() == null ? BigDecimal.ZERO : AmountCastUtil.castFen2Yuan2(t.getAmount()),
                    (k1, k2) -> k2));
        }

        // 会员间交易出账
        Long memberTradeOutAmount = separateOrderMapper.queryMemberTradeOutAmount(reconcileFundAccountDto.getAccountId(), context.getBeginTime(), context.getEndTime());
        // 会员间交易退款
        Long memberTradeRefundAmount = separateOrderMapper.queryMemberTradeRefundAmount(reconcileFundAccountDto.getAccountId(), context.getBeginTime(), context.getEndTime());
        // 提现
        Long withdrawAmount = withdrawOrderMapper.queryWithdrawAmount(reconcileFundAccountDto.getAccountId(), context.getBeginTime(), context.getEndTime());

        // 分账金额
        reconcileFundAccountDto.setShareAmount(tradeAmountMap.getOrDefault(FbankTradeCodeEnum.FTC_13.getCode(), BigDecimal.ZERO));
        // 分账撤销金额
        reconcileFundAccountDto.setShareCancelAmount(tradeAmountMap.getOrDefault(FbankTradeCodeEnum.FTC_14.getCode(), BigDecimal.ZERO));
        // 会员间交易入账
        reconcileFundAccountDto.setMemberTradeInAmount(tradeAmountMap.getOrDefault(FbankTradeCodeEnum.FTC_1.getCode(), BigDecimal.ZERO));
        // 会员间交易出账
        reconcileFundAccountDto.setMemberTradeOutAmount(AmountCastUtil.castFen2Yuan2(memberTradeOutAmount != null ? memberTradeOutAmount : 0L));
        // 会员间交易退款
        BigDecimal memberTotalTradeRefundAmount = AmountCastUtil.sum(
                AmountCastUtil.castFen2Yuan2(memberTradeRefundAmount != null ? memberTradeRefundAmount : 0L),
                tradeAmountMap.getOrDefault(FbankTradeCodeEnum.FTC_11.getCode(), BigDecimal.ZERO)
        );
        reconcileFundAccountDto.setMemberTradeRefundAmount(memberTotalTradeRefundAmount);
        // 可提现解冻
        reconcileFundAccountDto.setUnfreezeAmount(tradeAmountMap.getOrDefault(FbankTradeCodeEnum.FTC_12.getCode(), BigDecimal.ZERO));
        // 可提现冻结
        reconcileFundAccountDto.setFreezeAmount(tradeAmountMap.getOrDefault(FbankTradeCodeEnum.FTC_20.getCode(), BigDecimal.ZERO));
        // 提现金额
        reconcileFundAccountDto.setWithdrawAmount(AmountCastUtil.castFen2Yuan2(withdrawAmount != null ? withdrawAmount : 0L));

        // 支付 自营才有
        reconcileFundAccountDto.setPayAmount(BigDecimal.ZERO);
        // 退款 自营才有
        reconcileFundAccountDto.setRefundAmount(BigDecimal.ZERO);

        // 当日变动 = 入金 - 出金
        BigDecimal currentDayTotalInAmount = AmountCastUtil.sum(
                reconcileFundAccountDto.getMemberTradeInAmount(),
                reconcileFundAccountDto.getShareAmount(),
                reconcileFundAccountDto.getUnfreezeAmount()
        );
        BigDecimal currentDayTotalOutAmount = AmountCastUtil.sum(
                reconcileFundAccountDto.getMemberTradeOutAmount(),
                reconcileFundAccountDto.getMemberTradeRefundAmount(),
                reconcileFundAccountDto.getShareCancelAmount(),
                reconcileFundAccountDto.getFreezeAmount(),
                reconcileFundAccountDto.getWithdrawAmount()
        );
        reconcileFundAccountDto.setTotalActualChangeAmount(currentDayTotalInAmount.subtract(currentDayTotalOutAmount));
    }

    private void fillReconcileFundAccountDtoByPayReport(String logPrefix, ReconcileFundAccountDto reconcileFundAccountDto, ReconcileContext context) {
        Pair<BigDecimal, BigDecimal> payReportDayAmount = getPayReportDayAmount(logPrefix, reconcileFundAccountDto.getThirdUserId(), context);
        if (payReportDayAmount == null) {
            reconcileFundAccountDto.setLastDayBalance(BigDecimal.ZERO);
            reconcileFundAccountDto.setCurrentDayBalance(BigDecimal.ZERO);
            return;
        }
        reconcileFundAccountDto.setLastDayBalance(payReportDayAmount.getKey());
        reconcileFundAccountDto.setCurrentDayBalance(payReportDayAmount.getValue());
    }

    private Pair<BigDecimal, BigDecimal> getPayReportDayAmount(String logPrefix, String thirdUserId, ReconcileContext context) {
        List<String> payReportPayNos = Arrays.asList(
                String.join("|",
                        ReconcileTradeConstants.PAY_REPORT_PAYNO_PREFIX,
                        DateUtil.localDateTimeFormat(context.getBeginTime().minusDays(1)),
                        thirdUserId),
                String.join("|",
                        ReconcileTradeConstants.PAY_REPORT_PAYNO_PREFIX,
                        DateUtil.localDateTimeFormat(context.getEndTime().minusDays(1)),
                        thirdUserId)
        );

        List<PayReport> payReports = payReportMapper.queryListByPayNo(payReportPayNos);
        if (CollectionUtil.isEmpty(payReports)) {
            log.error("{} reconcileEcPopFundAccount payReports is empty, payReportPayNos:{}", logPrefix, JSON.toJSONString(payReportPayNos));
            return null;
        }
        if (payReports.size() != 2) {
            log.error("{} reconcileEcPopFundAccount payReports size error, payReportPayNos:{}", logPrefix, JSON.toJSONString(payReportPayNos));
            return null;
        }
        PayReport prevDayPayReport = payReports.get(0);
        PayReport currDayPayReport = payReports.get(1);
        return new Pair<>(prevDayPayReport.getResidual(), currDayPayReport.getResidual());
    }

    /**
     * key: account_id
     * value: third_user_id
     */
    private Map<String, String> getReconcilePopAccount(ReconcileContext context) {
        List<String> originalAccountIds = payAccountMapper.getAllEcPopAccountIds(context.getBusinessUserIds());
        if (CollectionUtil.isEmpty(originalAccountIds)) {
            return Collections.emptyMap();
        }

        // 过滤无效数据
        List<String> accountIds = originalAccountIds.stream().filter(this::filterInvalidAccountId)
                .distinct()
                .collect(Collectors.toList());
        if (CollectionUtil.isEmpty(accountIds)) {
            return Collections.emptyMap();
        }

        List<ThirdUserInfo> thirdUserInfos = new ArrayList<>();
        int maxLimit = 500;
        for (List<String> userIds : Lists.partition(accountIds, maxLimit)) {
            thirdUserInfos.addAll(Optional.ofNullable(thirdUserInfoMapper.getPinganThirdUserInfoByUserIds(userIds)).orElse(Collections.emptyList()));
        }
        if (CollectionUtil.isEmpty(thirdUserInfos)) {
            return Collections.emptyMap();
        }

        HashMap<String, String> resultMap = thirdUserInfos.stream()
                .filter(t -> StrUtil.isNotBlank(t.getUserId()) && StrUtil.isNotBlank(t.getThirdUserId()))
                .collect(Collectors.toMap(ThirdUserInfo::getUserId, ThirdUserInfo::getThirdUserId, (k1, k2) -> k2, HashMap::new));
        return resultMap;
    }

    private boolean filterInvalidAccountId(String accountId) {
        return StrUtil.isNotBlank(accountId) && !accountId.contains("delete");
    }

}
